﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using System.Xml.Linq;
using Bio;
using Bio.Algorithms.Alignment;
using Bio.Algorithms.Assembly;
using Bio.Algorithms.Translation;
using Bio.Encoding;
using Bio.IO.Fasta;
using Bio.IO.GenBank;
using Bio.IO.Gff;
using Bio.Util.Logging;
using Bio.Web;
using Bio.Web.Blast;
using QUT.Bio.SilverMap;
using QUT.Bio.SilverMap.ObjectModel;
using QUT.Bio.Util;
using Hit = QUT.Bio.SilverMap.ObjectModel.Hit;
using ISequence = QUT.Bio.SilverMap.ObjectModel.ILinearDomain;
using SequenceSource = QUT.Bio.SilverMap.ObjectModel.SequenceSource;

namespace BlastExplorer.SilverMapDataProducer {

	public class DataProvider: QUT.Bio.SilverMap.DataProvider {
		internal readonly ObjectModel model = new ObjectModel();
		private StatusBar statusBar;

		public const int MAX_TAXA = 20;

		static DataProvider () {
			AddBlastParameter( "Blast2Sequences", "BL2SEQ", "Blast 2 sequences" );
			AddBlastParameter( "Subjects", "SUBJECTS", "Subject sequences" );
			AddBlastParameter( "SubjectsFrom", "SUBJECTS_FROM", "Start position in subjects" );
			AddBlastParameter( "SubjectsTo", "SUBJECTS_TO", "End position in subjects" );
			AddBlastParameter( "BlastSpec", "BLAST_SPEC", "BlastSpec" );
			AddBlastParameter( "DB", "db", "db" );
			AddBlastParameter( "Taxon", "EQ_MENU", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon1", "EQ_MENU1", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon2", "EQ_MENU2", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon3", "EQ_MENU3", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon4", "EQ_MENU4", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon5", "EQ_MENU5", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon6", "EQ_MENU6", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon7", "EQ_MENU7", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon8", "EQ_MENU8", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon9", "EQ_MENU9", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon10", "EQ_MENU10", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon11", "EQ_MENU11", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon12", "EQ_MENU12", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon13", "EQ_MENU13", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon14", "EQ_MENU14", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon15", "EQ_MENU15", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon16", "EQ_MENU16", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon17", "EQ_MENU17", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon18", "EQ_MENU18", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "Taxon19", "EQ_MENU19", "Restrict search with taxonomic Ids" );
			AddBlastParameter( "CddSearch", "CDD_SEARCH", "Search CDD at the same time" );
		}

		private static void AddBlastParameter (
			string friendlyName,
			string ncbiName,
			string description
		) {
			if ( !BlastParameters.Parameters.ContainsKey( friendlyName ) ) {
				BlastParameters.Parameters.Add( friendlyName, new RequestParameter(
					ncbiName, description, false, "", "string", null
				) );
			}
		}

		public DataProvider ( StatusBar statusBar )
			: base( null ) {
			this.statusBar = statusBar;
		}

		#region Implement abstract base class.
		public override bool IsInteractive {
			get {
				return false;
			}
		}

		public override string Name {
			get {
				return "you shouldn't see this!";
			}
		}

		public override IFeatureCollection FeatureCollection {
			get {
				return model;
			}
		}

		public override void DisplayLoadDialog () {
			// Only needed if the data source is interactive.
		}

		public override void Initialise ( QUT.Bio.Util.Arguments args ) {
			// TODO: Review - may not be needed at all.
		}
		#endregion

		#region Private method: Load( IList<BlastResult>, Action)
		/// <summary>
		/// Loads the contents of a list of blast result collator objects into the model.
		/// <para>
		/// When finished, fires the LoadComplete event to notify listeners.
		/// </para>
		/// <param name="blastResults"></param>
		/// <param name="signal"></param>
		/// <param name="querySource"></param>
		/// <param name="hitSource"></param>
		/// </summary>

		public void Load (
			IList<Bio.Web.Blast.BlastResult> blastResults,
			Action signal,
			SequenceSource querySource,
			SequenceSource hitSource
		) {
			// Debug.WriteLine( String.Format( "Load starting." ) );

			foreach ( var blastResult in blastResults ) {
				foreach ( var blastSearchRecord in blastResult.Records ) {

					if ( blastSearchRecord.Hits.Count == 0 ) {
						continue;
					}

					Sequence querySequence = GetSequence(
						blastSearchRecord.IterationQueryId,
						blastSearchRecord.IterationQueryDefinition,
						blastSearchRecord.IterationQueryLength,
						querySource,
						SequenceKey.Empty
					);
// This is where the "User|1" name comes from
					if ( querySequence == null ) {
						SequenceKey queryKey = new SequenceKey( SequenceSource.User, blastSearchRecord.IterationNumber.ToString() );

						if ( model.sequences.ContainsKey( queryKey ) ) {
							querySequence = (Sequence) model.sequences[queryKey];
						}
						else {
							querySequence = new Sequence(
								querySource,
								queryKey.ToString(),
								"Unknown organism",
								"Query sequence",
								queryKey.ToString(),
								blastSearchRecord.IterationQueryLength
							);
							model.sequences[queryKey] = querySequence;
						}
					}

					if ( model.initialQuerySequence == null ) {
						model.initialQuerySequence = querySequence;
					}

					model.querySequences.Add( querySequence );

					foreach ( var hit in blastSearchRecord.Hits ) {

						Sequence hitSequence = GetSequence(
							hit.Id, hit.Def, hit.Length, hitSource, querySequence.Key
						);

						if ( hitSequence == null ) {
							continue;
						}

						foreach ( var hsp in hit.Hsps ) {
							IFeature queryFeature = model.features.Add( new DefaultFeature(
								querySequence,
								hsp.QueryStart,
								hsp.QueryEnd,
								hsp.QuerySequence
							) );

							IFeature hitFeature = model.features.Add( new DefaultFeature(
								hitSequence,
								hsp.HitStart,
								hsp.HitEnd,
								hsp.HitSequence
							) );

							model.hits[queryFeature, hitFeature] = new Hit( queryFeature, hitFeature ) {
								EValue = hsp.EValue,
								Identities = hsp.IdentitiesCount,
								Midline = Hit.SEQUENCE_DATA_UNAVAILABLE /* hsp.Midline */,
								Positives = hsp.PositivesCount
							};
						}
					}
				}
			}

			signal();

			// Debug.WriteLine( String.Format( "Load finishing." ) );
		}
		#endregion

		#region Private method: GetSequence
		/// <summary>
		/// Searches for the specified sequence by id in the object model.
		/// <para>
		///		If the sequence already exists, that object is returned.
		/// </para>
		/// <para>
		///		If the sequence does not exist in the collection, then we 
		///		create a new instance, add it to the collection, and return 
		///		it for use by the consumer.
		/// </para>
		/// </summary>
		/// <param name="id">A sequence identifier.</param>
		/// <param name="description">The name of the sequence.</param>
		/// <param name="length">The length of the sequence.</param>
		/// <param name="source">The name of the remote database that supplies sequence data.</param>
		/// <returns>See summary.</returns>

		private Sequence GetSequence (
			string id,
			string description,
			long length,
			SequenceSource source,
			SequenceKey queryKey
		) {
			try {
				if ( source == SequenceSource.Genbank ) {
					return GetGenbankSequence( id, description, length, queryKey );
				}
				else if ( source == SequenceSource.Cdd ) {
					return GetCddSequence( id, description, length );
				}
				else {
					return null;
				}
			}
			catch ( Exception e ) {
				Debug.WriteLine( "Exception caught: " + e );
				return null;
			}
		}
		#endregion

		#region Method: GetGenbankSequence ( string, string, long ) -> ISequence
		private Sequence GetGenbankSequence (
			string id,
			string description,
			long length,
			SequenceKey queryKey
		) {
			// TODO: add extra xrefs; at present we only keep the GI.

			string defLine = ">" + ( id.StartsWith( "lcl|" ) ? description : id + " " + description );

			string [] parts = defLine.Split( new char[] { '>' }, StringSplitOptions.RemoveEmptyEntries );

			//Regex regex = new Regex( "\\>gi\\|(?'gi'[^\\|]*)[^ ]* (?'product'[^\\[]*)\\[(?'chromosome'[^]]*)] *" );
			//MatchCollection matches = regex.Matches( defLine );

			Xref primaryXref = null;
			List<QUT.Bio.Util.Xref> xrefs = new List<QUT.Bio.Util.Xref>();

			Sequence result = null;

			foreach ( string part in parts ) {
				int firstSpace = part.IndexOf( ' ' );
				string [] idFragments = part.Substring( 0, firstSpace ).Split( '|' );
				string [] descriptionAndOrganism = part.Substring( firstSpace + 1 ).Split( '[', ']' );
				string product = descriptionAndOrganism.Length < 1 ? null : descriptionAndOrganism[0];
				string organism = descriptionAndOrganism.Length < 2 ? null : descriptionAndOrganism[1];

				#region Find the Gi number, and when found, use it to create the primaryXref. If not found, skip this entry.
				for ( int i = 0; i + 1 < idFragments.Length; i += 2 ) {
					if ( string.Equals( idFragments[i], "gi", StringComparison.InvariantCultureIgnoreCase ) ) {
						string gi = idFragments[i + 1];
						Xref newXref = new Xref( "GI", gi, product, organism );
						xrefs.Add( newXref );

						if ( primaryXref == null ) {
							primaryXref = newXref;
						}
					}
					else {
						xrefs.Add( new Xref( idFragments[i], idFragments[i + 1], product, organism ) );
					}
				}

				if ( primaryXref == null ) {
					continue;
				}
				#endregion
			}

			SequenceKey key = new SequenceKey( SequenceSource.Genbank, primaryXref.Id );

			if ( model.sequences.ContainsKey( key ) ) {
				result = (Sequence) model.sequences[key];
			}
			else {
				result = new Sequence(
					SequenceSource.Genbank,
					primaryXref.Id,
					primaryXref.Organism,
					primaryXref.Description,
					primaryXref.Id,
					length
				);

				model.sequences.Add( key, result );
			}

			Xref firstXref = (Xref) xrefs.FirstOrDefault( delegate( QUT.Bio.Util.Xref xref ) {
				return xref.Database == "GI" && queryKey.Source == SequenceSource.Genbank && xref.Id == queryKey.Id;
			} );

			if ( firstXref != null ) {
				result.EmbeddedQuerySequences.Add( queryKey );
			}

			List<QUT.Bio.Util.Xref> resultXrefs = (List<QUT.Bio.Util.Xref>) result.Xref;

			foreach ( Xref xref in xrefs ) {
				if ( !resultXrefs.Contains( xref ) ) {
					resultXrefs.Add( xref );
				}
			}

			return result;
		}
		#endregion

		#region Method: GetCddSequence ( string, string, long ) -> ISequence
		private Sequence GetCddSequence (
			string id,
			string description,
			long length
		) {
			if ( !id.StartsWith( "gnl" ) ) {
				throw new ArgumentException( "The id is expected to look like 'gnl|CDD|nnnnnn' " );
			}

			Sequence result = null;

			string [] idFragments = id.Split( '|' );
			string database = idFragments[1];
			string cddId = idFragments[2];

			Xref primaryXref = new Xref( database, cddId, description, "Conserved domain" );
			SequenceKey key = new SequenceKey( SequenceSource.Cdd, cddId );

			if ( model.sequences.ContainsKey( key ) ) {
				result = (Sequence) model.sequences[key];
			}
			else {
				result = new Sequence(
					SequenceSource.Cdd,
					cddId,
					"Conserved domain",
					description,
					key.ToString(),
					0
				);
				model.sequences.Add( key, result );
			}

			return result;
		}
		#endregion

		#region Method: Clear
		/// <summary>
		/// Removes all features, hits, sequences from the data collection managed by 
		/// this data producer.
		/// </summary>

		public void Clear () {
			model.RequestIds.Clear();
			model.features.Clear();
			model.hits.Clear();
			model.sequences.Clear();
			model.initialQuerySequence = null;
			model.querySequences.Clear();
			SignalLoadComplete();
		}
		#endregion

		#region Blast sequence against database
		#region Method: DoBlast
		/// <summary>
		/// Does an asynchronous blast against the specified database.
		/// </summary>
		/// <param name="query">the query, which may be a list of gi's, accessions, a single sequence or a FASTA formated list of sequences.</param>
		/// <param name="database">The NCBI Blast database to search.</param>
		/// <param name="eValue">The eValue cutoff for the search</param>
		/// <param name="numberOfHits">The maximum number of hits requested.</param>
		/// <param name="dispatcher">A dispatcher that will schedule the asynchronous Blast web call.</param>
		/// <param name="useBrowserProxy">Controls whether or not UseBrowserProxy will be set in the blast request.</param>
		/// <param name="program">The blast program (blastp, blastn will do for the moment) to execute.</param>
		/// <param name="whenItsAllOver">A callback to fix things up when all is done. This may be null if you do ot want to do anything.</param>

		public void DoBlast (
			string query,
			string program,
			string database,
			double eValue,
			int numberOfHits,
			Taxon[] taxa,
			Dispatcher dispatcher,
			bool useBrowserProxy,
			Action onSuccess,
			Action onFailure,
			SequenceSource querySource
		) {
			statusBar.WriteLine( "Calling NCBI BLAST" );
			statusBar.StartProgress();

			Thread t = new Thread( delegate() {

				DoBlast( query, program, database, eValue, numberOfHits, taxa, useBrowserProxy,
					delegate( List<Bio.Web.Blast.BlastResult> results ) {
						// This will be executed by the dispatcher back on the gui thread.
						statusBar.StopProgress();
						statusBar.Clear();

						if ( results.Count > 0 ) {
							Load( results, SignalCollectionModified, querySource, SequenceSource.Genbank );
						}

						if ( onSuccess != null ) {
							onSuccess();
						}
					},
					delegate( string message ) {
						// This will be executed by the dispatcher back on the gui thread.
						statusBar.StopProgress();
						statusBar.WriteLine( message );
						System.Windows.MessageBox.Show( message );

						if ( onFailure != null ) {
							onFailure();
						}
					}
				);
			} );

			t.Start();
		}
		#endregion

		#region Method: DoBlast
		/// <summary>
		/// Does a synchronous BLAST run, then uses the provided dispatcher to execute the
		/// supplied callback in the GUI thread.
		/// </summary>
		/// <param name="query"></param>
		/// <param name="program"></param>
		/// <param name="database"></param>
		/// <param name="eValue"></param>
		/// <param name="numberOfHits"></param>
		/// <param name="dispatcher"></param>
		/// <param name="useBrowserProxy"></param>
		/// <param name="onComplete"></param>
		/// <param name="taxon">restricts search to tax id or common scientific name.</param>

		private void DoBlast (
			string query,
			string program,
			string database,
			double eValue,
			int numberOfHits,
			Taxon[] taxa,
			bool useBrowserProxy,
			Action<List<Bio.Web.Blast.BlastResult>> onComplete,
			Action<string> onFailure
		) {
			// Configure the service request
			NCBIBlastHandler service = new NCBIBlastHandler();

			service.Configuration = new ConfigParameters {
				UseBrowserProxy = useBrowserProxy
			};

			BlastParameters parms = new BlastParameters();

			// Fill in the BLAST settings
			parms.Add( "Program", program );
			parms.Add( "Database", database );
			parms.Add( "Expect", eValue.ToString() );
			parms.Add( "CompositionBasedStatistics", "0" );
			parms.Add( "Query", query );
			parms.Add( "HitlistSize", numberOfHits.ToString() );

			if ( taxa != null && taxa.Length > 0 ) {
				parms.Add( "Taxon", taxa[0].TaxId.ToString() );

				for ( int i = 1; i < MAX_TAXA && i < taxa.Length; i++ ) {
					parms.Add( "Taxon" + i, taxa[i].TaxId.ToString() );
				}
			}

			try {
				Bio.ISequence dummy = null;
				string jobID = service.SubmitRequest( dummy, parms );
				ServiceRequestInformation info = service.GetRequestStatus( jobID );

				model.RequestIds.Add( jobID );

				if ( info.Status != ServiceRequestStatus.Waiting &&
					info.Status != ServiceRequestStatus.Ready ) {
					Alert.Show( "Error in request:{0}", info.StatusInformation );
				}

				// Get async results. Poll until the results are ready
				int maxAttempts = 10;
				int attempt = 1;
				List<BlastResult> results = null;

				while ( attempt <= maxAttempts ) {
					results = (List<BlastResult>) service.FetchResultsSync( jobID, parms );

					if ( results != null ) {
						break;
					}

					System.Threading.Thread.Sleep( results == null ? 5000 : 0 );
					++attempt;
				}

				if ( results == null ) {
					Application.Current.Dispatcher.BeginInvoke( onFailure, "Blast request timed out" );
				}
				else {
					Application.Current.Dispatcher.BeginInvoke( onComplete, results );
				}
			}
			catch ( Exception e ) {
				Application.Current.Dispatcher.BeginInvoke( onFailure, string.Format( "Blast request has failed:\n{0}", e ) );
			}
		}
		#endregion
		#endregion

		#region Blast sequence against other sequences
		#region Method: DoBlast
		/// <summary>
		/// Does an asynchronous blast against the specified database.
		/// </summary>
		/// <param name="query">the query, which may be a list of gi's, accessions, a single sequence or a FASTA formated list of sequences.</param>
		/// <param name="database">The NCBI Blast database to search.</param>
		/// <param name="eValue">The eValue cutoff for the search</param>
		/// <param name="numberOfHits">The maximum number of hits requested.</param>
		/// <param name="dispatcher">A dispatcher that will schedule the asynchronous Blast web call.</param>
		/// <param name="useBrowserProxy">Controls whether or not UseBrowserProxy will be set in the blast request.</param>
		/// <param name="program">The blast program (blastp, blastn will do for the moment) to execute.</param>
		/// <param name="whenItsAllOver">A callback to fix things up when all is done. This may be null if you do ot want to do anything.</param>

		public void Bl2Seq (
			Sequence query,
			string subjects,
			string program,
			string database,
			double eValue,
			int numberOfHits,
			Dispatcher dispatcher,
			bool useBrowserProxy,
			Action whenItsAllOver
		) {
			statusBar.WriteLine( "Calling NCBI Bl2Seq" );
			statusBar.StartProgress();

			Thread t = new Thread( delegate() {
				Bl2Seq( query, subjects, program, database, eValue, numberOfHits, dispatcher, useBrowserProxy,
					delegate( List<Bio.Web.Blast.BlastResult> results ) {
						statusBar.StopProgress();
						statusBar.Clear();

						if ( results.Count > 0 ) {
							Load( results, SignalCollectionModified, query.Source, SequenceSource.Genbank );
						}

						if ( whenItsAllOver != null ) {
							whenItsAllOver();
						}
					}
				);
			} );

			t.Start();
		}
		#endregion

		#region Method: DoBlast
		/// <summary>
		/// Does a synchronous BLAST run, then uses the provided dispatcher to execute the
		/// supplied callback in the GUI thread.
		/// </summary>
		/// <param name="query"></param>
		/// <param name="program"></param>
		/// <param name="database"></param>
		/// <param name="eValue"></param>
		/// <param name="numberOfHits"></param>
		/// <param name="dispatcher"></param>
		/// <param name="useBrowserProxy"></param>
		/// <param name="onComplete"></param>

		private void Bl2Seq (
			Sequence query,
			string subjects,
			string program,
			string database,
			double eValue,
			int numberOfHits,
			Dispatcher dispatcher,
			bool useBrowserProxy,
			Action<List<Bio.Web.Blast.BlastResult>> onComplete
		) {
			NCBIBlastHandler blast = new NCBIBlastHandler();

			blast.Configuration = new ConfigParameters {
				UseBrowserProxy = useBrowserProxy,
				Connection = new Uri( "http://www.ncbi.nlm.nih.gov/blast/BlastAlign.cgi" )
			};

			BlastParameters parms = new BlastParameters();

			parms.Add( "Query", query.Source == SequenceSource.Genbank ? query.Id : query.SequenceData );
			parms.Add( "Subjects", subjects );
			parms.Add( "Database", "nr" );
			parms.Add( "Program", program );
			parms.Add( "Blast2Sequences", "on" );
			parms.Add( "BlastSpec", "blast2seq" );
			parms.Add( "Expect", eValue.ToString() );
			parms.Add( "GapCosts", "11 1" );
			parms.Add( "DB", "protein" );
			parms.Add( "HitlistSize", numberOfHits.ToString() );

			try {
				Bio.ISequence dummy = null;
				string jobId = blast.SubmitRequest( dummy, parms );

				// Query the status
				ServiceRequestInformation info = blast.GetRequestStatus( jobId );

				if ( info.Status != ServiceRequestStatus.Waiting &&
					info.Status != ServiceRequestStatus.Ready ) {
					// Handle the unexpected error
				}

				// Get async results. Poll until results become available.
				int maxAttempts = 10;
				int attempt = 1;
				List<BlastResult> results = null;

				while ( attempt <= maxAttempts ) {
					results = (List<BlastResult>) blast.FetchResultsSync( jobId, parms );

					if ( results != null ) {
						break;
					}

					System.Threading.Thread.Sleep( 5000 );
					++attempt;
				}

				if ( results != null ) {
					dispatcher.BeginInvoke( onComplete, results );
				}
				else {
					dispatcher.BeginInvoke( new Action( delegate() {
						Alert.Show( "Blast request timed out." );
					} ) );
				}
			}
			catch ( Exception e ) {
				dispatcher.BeginInvoke( new Action( delegate() {
					Alert.Show( "Error while accessing NCBI Blast:\n{0}", e );
				} ) );
			}
		}
		#endregion
		#endregion

		#region Property: Xml
		/// <summary>
		/// Get or set an Xml encoding of this object.
		/// <para>The seter will clear then reload the object model.</para>
		/// </summary>

		public XElement Xml {
			get {
				return model.Xml;
			}
			set {
				Clear();
				model.Xml = value;
				SignalLoadComplete();
			}
		}
		#endregion
	}
}
